第4章 Spring Boot 整合Thymeleaf模板引擎
技术栈:Spring Boot 4.1.1 + JDK 17 + Thymeleaf 3.1.x 定位:服务端页面渲染入门,本章重点:从模板引擎基础到常用语法、表单绑定、模板布局、案例实战,配套完整动手练习
学习目标
- ✅ 理解模板引擎作用,掌握Spring Boot整合Thymeleaf的项目搭建
- ✅ 熟练掌握Thymeleaf核心属性:
th:text、th:each、th:if、th:href等常用语法 - ✅ 掌握变量、选择、链接、消息、片段五大标准表达式
- ✅ 掌握表单数据绑定、自动回显、下拉/单选/复选框处理
- ✅ 会使用
th:fragment做页面公共片段抽取与复用 - ✅ 了解国际化配置、模板布局企业最佳实践
- ✅ 能独立完成图书管理系统模板页面开发,排查常见入门问题
4.1 先搞明白:什么是模板引擎
4.1.1 为什么需要模板引擎
前面我们写的接口都返回JSON数据,如果需要服务端渲染HTML页面返回浏览器,总不能用字符串拼接HTML:
// 笨办法:字符串拼接
@GetMapping("/hello")
public String hello() {
String name = "张三";
return "<html><body><h1>你好," + name + "!</h1></body></html>";
}字符串拼接的痛点:
- 页面复杂时代码极长,维护困难
- 没有语法高亮,写错标签很难排查
- 前端无法协作设计
- 容易出现XSS安全漏洞
模板引擎就是解决这个问题的:
提前写好HTML模板骨架,预留动态数据占位符;程序把数据传给模板引擎,引擎把数据填充到对应位置,最终生成完整HTML页面返回浏览器。 公式:模板 + 数据 → 模板引擎 → 完整HTML页面
如果你学过JSP,那Thymeleaf和JSP是同类工具,只是语法更优雅、功能更强、和Spring无缝集成。
4.1.2 Spring Boot支持的模板引擎
Spring Boot对主流模板引擎都提供了starter启动器:
| 模板引擎 | 特点 | 企业使用情况 |
|---|---|---|
| Thymeleaf | 自然模板、语法优雅、Spring官方推荐 | 目前主流,新项目首选 |
| FreeMarker | 老牌模板引擎,功能强大 | 逐步减少,老项目维护 |
| Velocity | 更老的模板引擎 | 基本不用 |
| JSP | 传统Java Web技术 | Spring Boot不推荐,打包部署麻烦 |
💡 Spring Boot 4.x 内置Thymeleaf 3.1.x版本,引入starter自动配置,开箱即用。
4.1.3 为什么选Thymeleaf
优点一:自然模板(最大特色)
Thymeleaf模板文件本身就是合法的HTML文件,直接用浏览器打开就能看到静态原型效果;运行在服务器上,又能渲染动态数据。
JSP直接用浏览器打开是一堆乱码标签
Thymeleaf直接打开是正常页面,只是数据是示例原型
意义:前端设计师可以直接出页面,后端只需要加语法,前后端协作更顺畅。
优点二:语法优雅
所有动态语法都以th:开头的HTML属性形式存在,页面结构干净,不破坏HTML本身的结构。
优点三:和Spring无缝集成
Spring Boot官方推荐,自动配置,支持Spring MVC表单绑定、校验、国际化、安全集成。
优点四:功能丰富
条件判断、循环、模板布局、表单处理、国际化、Spring Security权限标签等。
4.2 快速入门:第一个Thymeleaf页面
4.2.1 引入依赖
创建Spring Boot项目,添加依赖,或者在已有项目pom.xml加入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>🎯 引入starter后Spring Boot自动完成配置:
- 默认模板位置:
src/main/resources/templates/- 默认文件后缀:
.html- 默认编码:UTF-8
- 生产环境默认开启缓存
4.2.2 编写Controller
⚠️ **新手第一坑:返回页面用
@Controller,不是@RestController!
@RestController= @Controller + @ResponseBody,返回JSON字符串@Controller:返回视图页面
创建controller包,新建HelloController.java:
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.ui.Model;
@Controller // 注意:这里是 @Controller,不是 @RestController!
public class HelloController {
@GetMapping("/hello")
public String hello(Model model) {
// 往模型里放数据,模板里可以取出来
model.addAttribute("name", "张三");
model.addAttribute("message", "欢迎学习 Thymeleaf!");
// 返回视图名称(模板文件名,不用加 .html)
return "hello";
}
}4.2.3 编写模板页面
在src/main/resources/templates/目录下,新建hello.html:
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>第一个Thymeleaf页面</title>
</head>
<body>
<h1>第一个Thymeleaf页面</h1>
<p>姓名:<span th:text="${name}">默认值</span></p>
<p>消息:<span th:text="${message}">默认消息</span></p>
</body>
</html>代码讲解:
xmlns:th="http://www.thymeleaf.org":声明Thymeleaf命名空间,编辑器识别th:属性,建议加上th:text="${name}":设置标签的文本内容,替换标签内所有内容- 标签中间的"李四"、"默认消息":原型默认值,直接浏览器打开时显示;服务器运行时替换成真实数据
✨ 这就是「自然模板」特性:
- 直接打开
hello.html:看到李四、默认消息(原型效果)- 通过服务器访问
/hello:看到张三、欢迎学习Thymeleaf!(真实数据)
4.2.4 启动测试
启动项目,浏览器访问:http://localhost:8080/hello 看到动态渲染的内容,第一个Thymeleaf页面就跑通了。
4.2.5 开发环境配置(必改)
默认Thymeleaf开启缓存,改页面不重启不会生效,开发体验很差。 application.yml添加配置:
spring:
thymeleaf:
cache: false # 开发环境关闭缓存,改完刷新浏览器就生效
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
mode: HTML💡 最佳实践:
- 开发环境:
cache: false,热更新方便- 生产环境:
cache: true,性能大幅提升- 配合Profile多环境配置,不同环境自动切换
4.3 Thymeleaf常用核心属性(重点⭐)
| 属性 | 作用 | 常用程度 |
|---|---|---|
th:text | 设置标签文本(自动转义,防XSS) | ⭐⭐⭐⭐⭐ |
th:utext | 设置标签文本(不转义) | ⭐⭐ |
th:value | 设置表单元素value值 | ⭐⭐⭐⭐⭐ |
th:each | 循环遍历集合 | ⭐⭐⭐⭐⭐ |
th:if | 条件成立才显示元素 | ⭐⭐⭐⭐⭐ |
th:unless | 条件不成立才显示元素 | ⭐⭐⭐⭐ |
th:switch/th:case | 多分支条件判断 | ⭐⭐ |
th:href | 设置a标签链接地址 | ⭐⭐⭐⭐⭐ |
th:src | 设置img标签图片地址 | ⭐⭐⭐⭐ |
th:action | 设置form表单提交地址 | ⭐⭐⭐⭐ |
th:class | 设置class属性 | ⭐⭐⭐⭐ |
th:style | 设置style属性 | ⭐⭐⭐ |
th:attr | 通用自定义属性 | ⭐⭐ |
4.3.1 th:text / th:utext
<!-- th:text:自动转义HTML特殊字符,安全防XSS -->
<p th:text="${content}">默认文本</p>
<!-- th:utext:不转义,HTML标签直接生效 -->
<p th:utext="${content}">默认文本</p>⚠️ 安全提醒:
- 用户输入的内容、数据库取出的不确定内容,必须用th:text,防止XSS攻击
- 只有确定100%安全的内容,才可以用
th:utext
4.3.2 th:value
给input等表单元素设置value值:
<input type="text" name="username" th:value="${username}" value="默认值">4.3.3 th:each 循环(最常用)
遍历集合生成多个元素,列表、表格必用。
基础用法
Controller放数据:
@GetMapping("/users")
public String users(Model model) {
List<User> users = Arrays.asList(
new User(1, "张三", 25),
new User(2, "李四", 28),
new User(3, "王五", 22)
);
model.addAttribute("users", users);
return "users";
}模板:
<table border="1">
<thead>
<tr><th>ID</th><th>姓名</th><th>年龄</th></tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.id}">1</td>
<td th:text="${user.name}">张三</td>
<td th:text="${user.age}">25</td>
</tr>
</tbody>
</table>循环状态变量
th:each可以带状态变量,获取索引、计数、奇偶行等信息:
<tr th:each="user, status : ${users}">
<td th:text="${status.index}">0</td>
<!-- 索引从0开始 -->
<td th:text="${status.count}">1</td>
<!-- 计数从1开始 -->
<td th:text="${status.size}">3</td>
<!-- 集合总大小 -->
<td th:text="${status.first}">true</td>
<!-- 是否第一个 -->
<td th:text="${status.last}">false</td>
<!-- 是否最后一个 -->
<td th:text="${status.even}">false</td>
<!-- 是否偶数行 -->
<td th:text="${status.odd}">true</td>
<!-- 是否奇数行 -->
<td th:text="${user.name}">张三</td>
</tr>隔行变色示例
<tr th:each="user, status : ${users}"
th:class="${status.even} ? 'even-row' : 'odd-row'">
<td th:text="${user.name}"></td>
</tr>配合CSS:偶数行even-row、奇数行odd-row不同背景色。
4.3.4 th:if / th:unless
th:if:条件成立,元素才显示th:unless:条件不成立,元素才显示(和if相反)
<!-- 成年才显示 -->
<p th:if="${age >= 18}">你已经成年了</p>
<!-- 未成年才显示 -->
<p th:unless="${age >= 18}">你还未成年</p>
<!-- 判断对象不为空 -->
<div th:if="${user != null}" th:text="${user.name}"></div>支持的运算符
| 运算符 | 说明 | 示例 |
|---|---|---|
gt / > | 大于 | ${age gt 18} |
lt / < | 小于 | ${age lt 18} |
ge / >= | 大于等于 | ${age ge 18} |
le / <= | 小于等于 | ${age le 18} |
eq / == | 等于 | ${age eq 18} |
ne / != | 不等于 | ${age ne 18} |
and | 并且 | ${age>18 and age<60} |
or | 或者 | ${age<18 or age>60} |
! / not | 非 | ${!isVip} |
💡 提示:HTML属性里写
<>可能有语法问题,推荐用gt、lt文本形式。
4.3.5 th:switch / th:case
多分支判断,类似Java switch-case:
<div th:switch="${role}">
<p th:case="'admin'">管理员</p>
<p th:case="'user'">普通用户</p>
<p th:case="'vip'">VIP会员</p>
<p th:case="*">未知角色</p>
<!-- * 代表默认分支 -->
</div>4.3.6 th:href + 链接表达式
配合@{...}链接表达式,自动处理上下文路径:
<!-- 基础链接 -->
<a th:href="@{/user/list}">用户列表</a>
<!-- 带参数 -->
<a th:href="@{/user/detail(id=1)}">详情</a>
<!-- RESTful路径参数 -->
<a th:href="@{/user/{id}/detail(id=${userId})}">详情</a>好处:项目有上下文路径时,自动帮你加上,不用手动拼接。
4.3.7 其他常用属性
th:src:设置图片src属性th:action:设置form表单action地址th:class:动态设置class属性th:style:动态设置style样式th:attr:设置任意自定义属性
4.4 标准表达式
Thymeleaf有5种标准表达式,各有用途:
| 表达式 | 语法 | 作用 | 常用程度 |
|---|---|---|---|
| 变量表达式 | ${...} | 取变量值、运算、调用方法 | ⭐⭐⭐⭐⭐ |
| 选择变量表达式 | *{...} | 取选定对象的属性 | ⭐⭐⭐⭐ |
| 链接表达式 | @{...} | 生成链接地址 | ⭐⭐⭐⭐⭐ |
| 消息表达式 | #{...} | 读取国际化消息 | ⭐⭐ |
| 片段表达式 | ~{...} | 引用模板片段 | ⭐⭐⭐⭐ |
4.4.1 变量表达式 ${...}
最常用,读取变量、做运算、调用方法:
<!-- 取对象属性 -->
<p th:text="${user.name}"></p>
<!-- 算术运算 -->
<p th:text="${price * quantity}"></p>
<!-- 三元运算 -->
<p th:text="${age >= 18} ? '成年' : '未成年'"></p>
<!-- 调用方法 -->
<p th:text="${user.getName()}"></p>
<p th:text="${users.size()}"></p>
<!-- 字符串拼接简写 |...| -->
<p th:text="|你好,${name},欢迎回来!|"></p>4.4.2 选择变量表达式 *{...}
配合th:object选定一个对象后,里面直接取对象属性,不用每次写前缀。
<!-- 普通写法 -->
<div>
<p th:text="${user.name}"></p>
<p th:text="${user.age}"></p>
</div>
<!-- 选择对象写法 -->
<div th:object="${user}">
<p th:text="*{name}"></p>
<p th:text="*{age}"></p>
</div>表单绑定场景用得最多,后面表单部分详细讲。
4.4.3 链接表达式 @{...}
生成链接,自动处理上下文路径,支持绝对路径、相对路径、带参数。
4.4.4 消息表达式 #{...}
读取国际化配置文件里的消息,国际化部分详细讲。
4.4.5 片段表达式 ~{...}
引用其他模板里的片段,做页面布局复用,模板布局部分详细讲。
4.5 内置对象与工具类
4.5.1 基础内置对象
不用往Model里放,模板里直接用:
| 对象 | 说明 |
|---|---|
#ctx | 上下文对象 |
#vars | 上下文变量 |
#request | HttpServletRequest对象 |
#response | HttpServletResponse对象 |
#session | HttpSession对象 |
#servletContext | ServletContext对象 |
示例:
<!-- 取session里的登录用户名 -->
<p>当前用户:<span th:text="${session.userName}">游客</span></p>
<!-- 取请求参数 -->
<p th:text="${#request.getParameter('keyword')}"></p>4.5.2 常用工具类
都是#开头,提供常用工具方法,不用自己写工具。
#strings 字符串工具
<!-- 判断为空 -->
<p th:if="${#strings.isEmpty(name)}">姓名为空</p>
<!-- 字符串长度 -->
<p th:text="${#strings.length(name)}"></p>
<!-- 转大小写 -->
<p th:text="${#strings.toUpperCase(name)}"></p>
<!-- 截取、替换、包含、开头结尾判断 -->
<p th:text="${#strings.substring(name,0,5)}"></p>
<p th:if="${#strings.contains(name,'张')}">包含张</p>#dates 日期工具
<!-- 格式化日期 -->
<p th:text="${#dates.format(birthday, 'yyyy-MM-dd HH:mm:ss')}"></p>
<!-- 获取年月日 -->
<p th:text="${#dates.year(birthday)}"></p>
<!-- 当前日期 -->
<p th:text="${#dates.createNow()}"></p>#numbers 数字工具
<!-- 保留2位小数 -->
<p th:text="${#numbers.formatDecimal(price, 1, 2)}"></p>
<!-- 百分比 -->
<p th:text="${#numbers.formatPercent(rate, 1, 2)}"></p>#lists 集合工具
<!-- 判空 -->
<p th:if="${#lists.isEmpty(users)}">列表为空</p>
<!-- 集合大小 -->
<p>共 <span th:text="${#lists.size(users)}">0</span> 条数据</p>
<!-- 是否包含 -->
<p th:if="${#lists.contains(users, user)}">包含</p>💡 不用全背,常用的记住,用到查官方文档即可。 官方文档:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#expression-utility-objects
4.6 表单处理(企业常用⭐⭐⭐)
Thymeleaf和Spring MVC表单绑定配合非常好,自动回显、自动绑定。
4.6.1 表单回显
编辑页面场景:打开页面,表单里自动显示原有数据。
Controller:
@GetMapping("/user/edit/{id}")
public String editUser(@PathVariable Integer id, Model model) {
// 模拟从数据库查询
User user = new User(id, "张三", 25, "zhangsan@example.com");
model.addAttribute("user", user);
return "user_edit";
}模板:
<form th:object="${user}" th:action="@{/user/save}" method="post">
<div>
<label>ID:</label>
<input type="text" th:field="*{id}" readonly>
</div>
<div>
<label>姓名:</label>
<input type="text" th:field="*{name}">
</div>
<div>
<label>年龄:</label>
<input type="number" th:field="*{age}">
</div>
<button type="submit">保存</button>
</form>🎯
th:object + th:field组合的作用:
- 自动设置
name、id属性- 自动设置
value值(表单回显)- 提交后Spring MVC自动封装成对象
4.6.2 表单提交
@PostMapping("/user/save")
public String saveUser(User user) {
// 保存用户业务逻辑
System.out.println("保存用户:" + user);
// 重定向到列表页,防止刷新重复提交
return "redirect:/user/list";
}💡 forward vs redirect:
- forward(转发):地址栏不变,刷新会重复提交
- redirect(重定向):地址栏变,刷新不会重复提交
- 表单提交后必须用重定向
4.6.3 下拉框、单选框、复选框
下拉框
<!-- 固定下拉 -->
<select th:field="*{gender}">
<option value="">请选择</option>
<option value="1">男</option>
<option value="2">女</option>
</select>
<!-- 从集合生成下拉 -->
<select th:field="*{deptId}">
<option value="">请选择部门</option>
<option th:each="dept : ${depts}"
th:value="${dept.id}"
th:text="${dept.name}"></option>
</select>单选框
<input type="radio" th:field="*{gender}" value="1"> 男
<input type="radio" th:field="*{gender}" value="2"> 女复选框
<!-- 单个布尔值 -->
<input type="checkbox" th:field="*{isVip}"> VIP
<!-- 多个值集合 -->
<input type="checkbox" th:field="*{hobbies}" value="reading"> 阅读
<input type="checkbox" th:field="*{hobbies}" value="music"> 音乐
<input type="checkbox" th:field="*{hobbies}" value="sports"> 运动选中状态自动回显,不用自己判断。
4.7 模板布局:页面复用
实际项目中,头部、底部、侧边栏都是公共的,每个页面都写一遍,维护麻烦。Thymeleaf支持抽取公共片段,其他页面引用。
4.7.1 定义片段 th:fragment
新建templates/fragments/header.html:
<div th:fragment="header">
<h1>我的网站</h1>
<nav>
<a th:href="@{/}">首页</a>
<a th:href="@{/book/list}">图书管理</a>
</nav>
<hr>
</div>新建templates/fragments/footer.html:
<div th:fragment="footer">
<hr>
<p>© 2026 我的网站 版权所有</p>
</div>4.7.2 三种引用方式
| 方式 | 效果 | 常用程度 |
|---|---|---|
th:insert | 片段插入到当前标签内部 | ⭐⭐⭐ |
th:replace | 片段替换整个当前标签 | ⭐⭐⭐⭐⭐ |
th:include | 只包含片段内容,不要根标签 | ⭐⭐ |
示例:
<body>
<!-- 替换当前div为header片段 -->
<div th:replace="~{fragments/header :: header}"></div>
<div class="content">
页面主体内容
</div>
<div th:replace="~{fragments/footer :: footer}"></div>
</body>4.7.3 带参数的片段
片段可以像函数一样传参数:
<!-- 定义带参片段 -->
<div th:fragment="greeting(name)">
<p>你好,<span th:text="${name}">朋友</span>!</p>
</div>
<!-- 引用传参 -->
<div th:replace="~{fragments/common :: greeting('张三')}"></div>4.7.4 企业最佳实践
- 公共头部、底部、侧边栏统一抽取到
fragments目录 - 业务页面统一引用,修改公共部分只改一个文件
- 不要嵌套太深,维护困难
4.8 国际化(了解)
让网站支持多语言切换。
4.8.1 配置消息文件
src/main/resources/i18n/目录下创建:
messages.properties(默认)messages_zh_CN.properties(中文)messages_en_US.properties(英文)
application.yml配置:
spring:
messages:
basename: i18n/messages
encoding: UTF-84.8.2 模板中使用
<h1 th:text="#{welcome.title}">欢迎</h1>
<p th:text="#{welcome.message}">欢迎来到我的网站</p>4.8.3 切换语言
配置LocaleResolver和拦截器,通过lang参数切换语言,了解即可。
4.9 案例实战:图书管理系统
通过完整案例把知识点串起来,跟着做一遍,Thymeleaf基本就入门了。
4.9.1 需求分析
做一个简单图书管理系统:
- 图书列表展示,显示所有图书
- 条件搜索:按书名或作者模糊搜索
- 借阅功能:点击借阅按钮,借阅图书
- 权限控制:管理员看到借阅按钮,普通用户看不到
- 顶部显示当前登录用户信息
4.9.2 项目搭建
创建Spring Boot项目,依赖:Spring Web、Thymeleaf、Lombok。
4.9.3 实体类
Book.java:
@Data
@AllArgsConstructor
public class Book {
private Integer id;
private String name;
private String author;
private String press;
private String status; // 0-可借阅 1-已借出 2-遗失
}User.java:
@Data
@AllArgsConstructor
public class User implements Serializable {
private String name;
private String role; // ADMIN-管理员 USER-普通用户
}4.9.4 模拟数据
Data.java模拟数据库:
public class Data {
public static List<Book> getBooks() {
List<Book> books = new ArrayList<>();
books.add(new Book(1, "楚辞", "屈原", "中国文联出版社", "0"));
books.add(new Book(2, "纳兰词", "纳兰性德", "中国文联出版社", "1"));
books.add(new Book(3, "西游记", "吴承恩", "中国文联出版社", "2"));
return books;
}
}4.9.5 Controller编写
BookController.java:
package com.example.demo.controller;
// 正确导入Spring MVC的Model!!
import org.springframework.ui.Model;
import com.example.demo.entity.Book;
import com.example.demo.entity.User;
import com.example.demo.model.Data;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.ArrayList;
import java.util.List;
@Controller
@RequestMapping("/book")
public class BookController {
// 模拟数据库图书数据
List<Book> books = Data.getBooks();
/**
* 查询所有图书
*/
@RequestMapping("/list")
public String list(Model model, HttpSession session) {
// 模拟用户登录(实际项目从登录接口获取)
session.setAttribute("user", new User("zhangSan", "ADMIN"));
// 把图书列表放入模型,模板中可以读取
model.addAttribute("books", books);
return "books";
}
/**
* 条件搜索:按书名/作者模糊搜索
*/
@RequestMapping("/search")
public String search(Book book, Model model, HttpSession session) {
List<Book> result = new ArrayList<>();
// 规范变量名:见名知意
String bookName = book.getName();
String bookAuthor = book.getAuthor();
// 两个条件都为空,返回全部
if ((bookName == null || bookName.isEmpty())
&& (bookAuthor == null || bookAuthor.isEmpty())) {
result = books;
} else {
// 按书名或作者模糊匹配
for (Book b : books) {
boolean nameMatch = bookName != null && !bookName.isEmpty()
&& b.getName().contains(bookName);
boolean authorMatch = bookAuthor != null && !bookAuthor.isEmpty()
&& b.getAuthor().contains(bookAuthor);
if (nameMatch || authorMatch) {
result.add(b);
}
}
}
session.setAttribute("user", new User("zhangSan", "ADMIN"));
model.addAttribute("books", result);
return "books";
}
/**
* 借阅图书
*/
@RequestMapping("/borrow/{id}")
public String borrow(@PathVariable("id") Integer id) {
// 实际项目中这里会修改数据库状态
System.out.println("借阅图书ID:" + id);
// 重定向到列表页,防止刷新重复提交
return "redirect:/book/list";
}
}4.9.6 列表页面实现
templates/books.html:
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>图书管理系统</title>
<style>
body { font-family: "微软雅黑"; margin: 20px; }
.header { display: flex; justify-content: space-between; align-items: center; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.status-0 { color: green; }
.status-1 { color: orange; }
.status-2 { color: red; }
.btn {
padding: 4px 10px; background: #007bff; color: white;
text-decoration: none; border-radius: 3px;
display: inline-block;
}
.btn-disabled {
padding: 4px 10px; background: #ccc; color: white;
border-radius: 3px; display: inline-block; cursor: not-allowed;
}
</style>
</head>
<body>
<!-- 头部 -->
<div class="header">
<h1>图书管理系统</h1>
<div th:if="${session.user != null}">
欢迎您,<span th:text="${session.user.name}">游客</span>
(<span th:text="${session.user.role}">USER</span>)
</div>
<div th:unless="${session.user != null}">
欢迎您,游客(未登录)
</div>
</div>
<!-- 搜索栏 -->
<div style="margin: 20px 0;">
<form th:action="@{/book/search}" method="get">
书名:<input type="text" name="name" th:value="${param.name[0] ?: ''}">
作者:<input type="text" name="author" th:value="${param.author[0] ?: ''}">
<button type="submit">搜索</button>
<a th:href="@{/book/list}" class="btn">重置</a>
</form>
</div>
<!-- 图书列表 -->
<table>
<thead>
<tr>
<th>编号</th>
<th>书名</th>
<th>作者</th>
<th>出版社</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="book : ${books}">
<td th:text="${book.id}">1</td>
<td th:text="${book.name}">楚辞</td>
<td th:text="${book.author}">屈原</td>
<td th:text="${book.press}">中国文联出版社</td>
<td>
<span th:if="${book.status == '0'}" class="status-0">可借阅</span>
<span th:if="${book.status == '1'}" class="status-1">已借出</span>
<span th:if="${book.status == '2'}" class="status-2">遗失</span>
</td>
<td>
<!-- 管理员才显示操作按钮 -->
<span th:if="${session.user.role == 'ADMIN'}">
<a th:if="${book.status == '0'}"
th:href="@{/book/borrow/{id}(id=${book.id})}"
class="btn">借阅</a>
<span th:if="${book.status != '0'}" class="btn-disabled">不可借</span>
</span>
<span th:unless="${session.user.role == 'ADMIN'}">无权限</span>
</td>
</tr>
</tbody>
</table>
<div style="margin-top: 20px;">
共 <span th:text="${#lists.size(books)}">0</span> 本图书
</div>
</body>
</html>4.9.7 运行测试
启动项目,访问http://localhost:8080/book/list 验证功能:
- 页面显示图书列表,状态不同颜色
- 搜索功能:输入关键词搜索对应图书
- 借阅功能:点击借阅,控制台打印ID
- 权限控制:修改角色为USER,操作列显示无权限
4.10 企业最佳实践与常见坑
4.10.1 前后端分离 vs 模板引擎
| 类型 | 适用场景 |
|---|---|
| 前后端分离 | 大型项目、复杂交互、多端共用、对体验要求高 |
| 模板引擎 | 中小型项目、后台管理系统、SEO要求高的页面、邮件模板 |
现状:互联网公司大多前后端分离;传统企业、后台管理系统很多仍用模板引擎。两种都要会。
4.10.2 使用规范
- 模板不写复杂业务逻辑:复杂计算、判断放到Controller,模板只负责展示
- 公共部分抽取复用:头部、底部统一抽取,不要每个页面重写
- XSS安全:用户输入内容用
th:text,绝对不要随便用th:utext - 缓存配置:开发关缓存,生产开缓存
- 配合Spring Security:
sec:标签可以直接在模板做权限判断
4.10.3 新手高频踩坑排查
| 问题 | 原因 | 解决 |
|---|---|---|
| 返回字符串不是页面 | @Controller写成@RestController | 返回页面用@Controller,返回JSON用@RestController |
| 模板找不到404 | 模板放错目录、文件名写错 | 模板必须在templates/目录,大小写拼写一致 |
| th:xxx不生效 | 直接访问HTML文件,没走Controller | 通过Controller访问,不要直接打开HTML |
| 改页面不生效 | 缓存开着 | 开发环境spring.thymeleaf.cache=false,重启项目 |
| 表达式空指针报错 | 对象为null还访问属性 | 先判断非空th:if="${user != null}",或用安全导航${user?.name} |
| 表单重复提交 | 用了forward转发 | 表单提交后用redirect:重定向 |
| th:field绑定失败 | Model里没有对应对象 | 确保Controller里addAttribute对应名称的对象 |
4.11 本章小结
- 基础认知:模板引擎作用,Thymeleaf自然模板、语法优雅、无缝集成三大特点
- 快速入门:引入starter、写Controller、写模板、开发关缓存
- 核心属性:
th:text、th:each、th:if、th:href、th:field等常用属性 - 标准表达式:变量、选择、链接、消息、片段五大表达式
- 表单处理:
th:object+th:field自动绑定回显,下拉单选复选 - 模板布局:
th:fragment定义片段,th:replace引用,页面复用 - 案例实战:图书管理系统完整实现,覆盖常用功能
4.12 课后作业
一、基础必做题(全员必做,动手实操)
- 第一个Thymeleaf页面 创建Spring Boot项目,编写
/hello接口,返回hello页面,显示“姓名:XXX”和“欢迎学习Spring Boot!”。 - 循环与条件练习 编写用户列表页面,遍历3个用户数据,表格展示; 实现隔行变色效果; 年龄≥18显示“成年”,否则显示“未成年”。
- 表单练习 编写用户编辑页面,表单回显用户ID、姓名、年龄、邮箱; 提交表单后,重定向到列表页面。
- 模板布局练习 抽取公共头部、底部片段; 用户列表页、编辑页都引用公共头尾。
- 图书管理案例 完整实现本章图书管理系统,跑通列表、搜索、借阅、权限控制全部功能。
二、进阶选做题(学有余力完成)
- 国际化:配置中英文两套消息,页面顶部增加中文/English切换按钮。
- 权限控制:结合session,管理员显示操作按钮,普通用户不显示。
- 分页组件:使用
th:each实现简单分页条组件,支持上一页、下一页、页码显示。
三、简答题(面试/笔试)
- Thymeleaf是什么?有什么核心特点?
th:text和th:utext有什么区别?哪个更安全,为什么?- Thymeleaf常用的表达式有哪些?分别有什么作用?
- 模板布局引用片段有哪几种方式?有什么区别?
- 为什么开发环境要关闭Thymeleaf缓存?生产环境呢?
- 返回页面应该用
@Controller还是@RestController?为什么新手容易搞混?
下一章我们学习Spring Boot数据访问,整合MyBatis操作数据库。